Све свеске се међусобно надограђују, тако да нам је неопходно да поновимо неке делове из претходних свески, што без додатног објашњења чинимо у овој секцији.
Овде нам је ради анализе резултата неопходан списак класа. Такође, да би смо могли да користимо модел поновићемо поступак из претходне свеске. Дакле, увеземо неопходне библиотеке и учитамо модел. Ово се наводи без објашњења, с обзиром да је поступак идентичан као у претходним свескама
classes = [ 'AnnualCrop',
'Forest',
'HerbaceousVegetation',
'Highway',
'Industrial',
'Pasture',
'PermanentCrop',
'Residential',
'River',
'SeaLake']
from os import environ
environ["OPENCV_IO_ENABLE_JASPER"] = "true"
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import cv2
import numpy as np
from skimage import exposure
from sklearn import metrics
from matplotlib import pyplot as plt
with torch.no_grad():
model = torchvision.models.resnet50(pretrained=True)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 10)
device = "cpu"
model.to(device)
model.load_state_dict(torch.load(r"mldata\resnet_50_land_use.pt", map_location=torch.device(device)))
model.eval()
print("Модел учитан")
Извршавање модела на примерима из базе¶
Учитавање и припрема базе¶
Најпре учитавамо базу података помоћу пакета torchvision. Овај корак је заправо јако једноставан ако је база адекватно представљена у фајл систему. Довољно је да за сваку од класа имамо фолдер који носи њено име, а унутар тог фолдера да се налазе одговарајуће слике. Када је база тако припремљена само користимо класу DatasetFolder да је учитамо без додатног труда.
Остатак ћелије испод садржи помоћне параметре за учитавање. Наиме неопхоно је дефинисати функцију која ће учитавати слике, јер се у зависности од примене и типа слике то може различито радити. У нашој функцији смо рецимо ми слике скалирали са 255.0 да би оне имале вредност у опсегу 0 до 1, што би значило да смо вредност сваког пиксела слике поделили са 255.0 што је максимална вредност коју пиксел може имати за тип слика које овде користимо.
Поред тога, над улазним сликама се могу извршити и различите трансформације, овај поступак је битнији за тренирање модела, али и сада је неопходно слике повећати на величину 224x224 коју очекује дата архитектура неуралне мреже.
def image_loader(path):
image = (cv2.imread(path).astype("float32") / 255.0)[:, :, ::-1].copy()
return torch.from_numpy(image.transpose(2,0,1))
transforms = torchvision.transforms.Compose([
torchvision.transforms.ToPILImage(),
torchvision.transforms.Resize(224),
torchvision.transforms.ToTensor(),
])
dataset = torchvision.datasets.DatasetFolder(root=r"mldata\EuroSAT\2750", loader=image_loader, transform=transforms, extensions="jpg")
Иако нећемо тренирати модел у овој вежбанци, проћићемо кроз веома битан поступак поделе скупа података на тренирајући, валидирајући и тестирајући.
Један од кључних проблема са којим се инжењер или научник који се бави машинским учењем сусреће је борба против "учења напамет". Слично као наставник у школи, ми желимо да из процеса обучавања модела извучемо суштинске концепте тако да се може применити и на примерима које није видео у току обуке. Дакле желимо да модел добро генерализује. Оно што не желимо је да модел просто меморише парове улаз-очекивани излаз без креирања генерализованих обележја и класификационог приступа. За случај учења напамет користи се стручан термин преобучавање (енг. overfitting), а све технике које се боре против преобучавања и доводе до боље генерализације се називају регуларизација.
Подела сета података са којим радимо на више скупова управо служи као алат за детекцију да ли је дошло до преобучавања. Наиме, сам модел се обучава на тренирајућем скупу података, а његова тачност се испитује на одвојеном скупу података који модел није видео током тренинга. Уколико модел даје значајно лошије предикције на издвојеном скупу података у односу на тренирајући то је вероватан знак да је дошло до преобучавања.
Питате се можда зашто се издвајају два скупа поред тренирајућег - скуп за валидацију и за тестирање. Наиме, у току развоја модела користи се скуп за валидацију где ће особа која ради на моделу доносити разне одлуке покушавајући да поправи резултат на валидацији. Тиме је могуће да се у току развоја модела унесу различите претпоставке и кроз њих у некој мери дође до преобучавања и на валидационом скупу података. Због тога имамо и тестирајући скуп података који се идеално користи само једном - онда када смо завршили развој модела и желимо да га испоручимо кориснику, где ћемо као тачност модела пријавити резултат на тестирајућем скупу, који ни модел ни особа која га је правила раније није видела.
Највећи део података се користи за тренирање, док се за валидацију и тестирање користе значајно мањи удели. Честа подела је 70%/15%/15% као што је и учињено у ћелији испод.
train_ratio = 0.7
val_ratio = 0.15
test_ratio = 0.15
dataset_size = len(dataset)
train_samples = int(train_ratio * dataset_size)
val_samples = int(val_ratio * dataset_size)
test_samples = dataset_size - train_samples - val_samples # Еквивалентно са int(val_ratio * test_ratio) али избегава грешку заокруживања.
print(f"Укупан број примера у скупу за тренирање: {train_samples}")
print(f"Укупан број примера у скупу за валидацију: {val_samples}")
print(f"Укупан број примера у скупу за тестирање: {val_samples}")
Пакет torch већ има функцију за поделу учитаног скупа података на дисјунктне подскупове, односно подскупове који немају никакав пресек (јер не би желели да нам неки од података из тестирајућег сета рецимо заврше у тренирајучем). Као улаз му само треба специфицирати удео сваког од скупа, што смо већ дефинисали у претходној ћелији. Поред тога, с обзиром да жељена функција из пакета torch, која врши поделу на подскупове, ту поделу ради насумично, односно сваки пут кад је позовемо добићемо другачију расподелу примера по тренирајућем, валидирајућем и тестирајућем скупу. Ово понашање није идеално за нас, па ћемо зато фиксирати семе генератора насумичних бројева да бисмо имали поновљивост - т.ј. да бисмо сваки пут када покренемо свеску имали исте тренинг, валидација и тест скупове.
DATASET_SEED = 12345
train_dataset, val_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_samples, val_samples, test_samples], generator=torch.Generator().manual_seed(DATASET_SEED))
Ради ефикасног учитавања података при тренирању и коришћењу мреже они се најчешће учитавају у хрпама (енг. batch). Поред тога неопходно је дефинисати и друге параметре самог процеса учитавања ради његове ефикасности. То се чини креирањем DataLoader. инстанце, а ми смо у примеру испод то урадили само за валидирајући скуп, с обзиром да у овој свесци нећемо тренирати модел. Такође дефинисали смо и да је величина појединачне хрпе која се учитава 32 слике.
BATCH_SIZE = 32
# train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0, drop_last=True, pin_memory=True)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, drop_last=True, pin_memory=True)
Хајде да учитамо неколико слика из валидирајућег скупа података и да их прикажемо. Команда испод нам довлачи следећу хрпу из валидационог сета. Наравно, довлаче се и одговарајуће лабеле.
example_batch, example_labels = next(iter(val_loader))
У следећој ћелији приказујемо облик хрпе података коју смо учитали. Препознајете да задње две димензије представљају величину слике од 224x224 пиксела коју неурална мрежа очекује.
Размислите, шта представљају прве две димензије?
print(example_batch.shape)
Напишите сами команду која приказује облик учитаних лабела. Да ли су дате димензије оно што очекујете? Зашто?
# Решење
# print(example_labels.shape)
Сада можемо проћи кроз целокупну хрпу података коју смо учитали и приказати сваку од слика заједно са пратећом лабелом.
Погледајте приказане слике и размислите која обележја би била добра да раздвоје дате класе.
batch_np = example_batch.numpy()
for i in range(BATCH_SIZE):
plt.figure(figsize=(2, 2))
plt.imshow(batch_np[i, ...].transpose(1, 2, 0))
plt.show()
print(dataset.classes[example_labels[i]])
Извршавање модела над појединачним примерима¶
Када имамо припремљене податке у облику у којем их модел очекује, можемо јако једноставно да добијемо предикцију за сваку од слика. Најпре селектујемо прву слику у хрпи example_batch[0:1, ...], где три тачке за остале координате означавају да их узимамо без измена. Модел онда позовемо као најобичнију функцију над тим подацима - резултат ће бити предикције.
with torch.no_grad():
prediction = model(example_batch[0:1, ...])
prediction
Погледајте резултат предикција - модел је за дату улазну слику дао 10 вредности на излазу. Као што смо раније рекли, свака од датих вредности одговара једној од класа. Саме те вредности представљају скорове, а у случају да нам је модел вештачка неурална мрежа, у питању су активације излазних неурона. Но, сасвим довољно је о овоме размишљати апстрактно: модел нам даје скорове за сваку од класа и тамо где је скор највећи та класа је оно што модел мисли да је добио на улазу.
Вежба 1: Утврдити сами коју класу је предвидео модел и да ли је та предикција тачна.
Вежба 2: Промените ћелије изнад тако да дају предикцију за другу по реду слику у хрпи. Поновите вежбу 1 за тај излаз.
У ћелијама испод ћемо програмским путем извести претходну вежбу и као излаз дати коју класу је модел предвидео.
Дакле, занима нас на ком положају у низу свих предикција се налази максимална вредност, јер знамо да редни број тог положаја одговара класи коју је модел предвидео - математичким формализмима речено - неопходан нам је argmax.
max_val, argmax = torch.max(prediction, 1)
argmax
Индексирање класа почиње од 0, а листа класа је дата у првој ћелији ове вежбанке. Хајде да видимо којој класи одговара предикција, а којој очекивана вредност (лабела).
print(classes[argmax])
print(classes[example_labels[0]])
Модел је тачно предвидео класу! Можете се сами играти и видети како предвиђа остале слике из учитане хрпе - модификацијом примера кода изнад.
Можда вам је већ интуитивно да, с обзиром да модел предвиђа скор за сваку од класа, је могуће дати и естимацију вероватноће са којом је модел сигуран у своју предикцију. У машинском учењу се за те потребе најчешће користи sofmax функција у коју нећемо даље залазити, али је довољно запамтити да она вектор скорова претвара у апроксимативну расподелу вероватноће.
with torch.no_grad():
softmax = F.softmax(prediction, dim=1)
Ако сада узмемо вредност апроксимативне вероватноће на месту предикције - видећемо да је модел практично потпуно сигуран у своју предикцију (вероватноћа је блиска 1). Ово је донекле и очекивано с обзиром да валидациони скуп података потпуно кореспондира тренирајућем. С друге стране, када дођемо до обраде података из Србије, видећете да модел неће бити тако сигуран у своје предикције.
print(float(softmax[0, argmax]))
Процесирање у хрпама (енг. batch processing)¶
Једна од супер моћи које нам омогућавају наменски процесори са пуно језгара (као што су графичке картице) је процесирање веће хрпе података у паралели. Тиме се за теоријски исто време одједном обрађује велика количина података.
Овакав вид обраде је најприроднији у пакету torch, и заправо је само довољно моделу проследити целокупан учитани batch и добићемо резултат. Слично, израчунавање argmax-a се спроводи без измена и над хрпом предикција.
with torch.no_grad():
predictions = model(example_batch)
max_val, argmax = torch.max(predictions, 1)
Хајде да упоредимо низ предвиђених индекса класа са лабелама за целу учитану хрпу.
print(argmax)
print(example_labels)
Пробајте сами да напишете команду која враћа укупан број предикција. Да ли је он очекиван и зашто?
# Решење
# len(argmax)
# ili
# argmax.shape
На исти начин као и раније ћемо за целу хрпу приказати апроксимативне расподеле вероватноћа израчунате softmax функцијом. Овај пут ћемо исштампати цео вектор. Видимо да за сваку од 32 улазне слике имамо по 10 вредности.
with torch.no_grad():
softmaxes = F.softmax(predictions, dim=1)
print(softmaxes)
Анализа сигурности модела¶
Као што смо навели, излаз softmax-а се може интерпретирати као апроксимативна расподела вероватноће. Команде испод израчунавају такву вероватноћу (скор) предикције за сваки од 32 улаза у мрежу.
Уочите која је најмања вероватноћа предикције - на већем скупу већ можемо уочити да на појединим примерима модел није потпуно сигуран.
all_scores = np.take_along_axis(softmaxes.numpy().T, np.expand_dims(argmax.numpy(), 0), 0)
print(all_scores)
С друге стране, ако израчунамо средњу вредност скора за све предикције, видимо да је модел у глобалу практично поптуно сигуран у своје предикције на валидационом скупу. Запамтите овај број - упоредићемо га са истим на подацима из Србије.
all_scores.mean()